#
# ped.py
#
# Python IDE for Nokia S60 platform.
#
# Copyright (c) 2007-2008, Arkadiusz Wahlig
# <arkadiusz.wahlig@gmail.com>
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
# * Neither the name of Arkadiusz Wahlig nor the names of its contributors may
#   be used to endorse or promote products derived from this software without
#   specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#


# application version
__version__ = '2.30.5 beta'


import sys
import e32
import os
import ui


symbols = [('()', 1),
           ('[]', 1),
           ('{}', 1),
           ("''", 1)]


statements = [('____', 2),
              ('break', None),
              ('class :', 6),
              ('class ():', 6),
              ('class (object):', 6),
              ('continue', None),
              ('def ():', 4),
              ('def (self):', 4),
              ('del ', None),
              ('elif :', 5),
              ('else:', None),
              ('except:', None),
              ('finally:', None),
              ('for  in :', 4),
              ('from  import *', 5),
              ('global ', None),
              ('if :', 3),
              ('import ', None),
              ('lambda :', 7),
              ('pass', None),
              ('print ', None),
              ('return', None),
              ('try:', None),
              ("u''", 2),
              ('while :', 6)]


class GlobalWindowModifier(object):    
    def __init__(self):
        self.shortcuts = {}

    def open(self):
        self.apply_settings()

    def focus_changed(self, focus):
        if focus:
            try:
                wnds_item = self.menu.find(title=_('Windows'))[0]
                wnds_item.submenu = ui.screen.get_windows_menu()
                wnds_item.submenu.sort()
                wnds_item.hidden = not len(wnds_item.submenu)
                self.update_menu()
            except IndexError:
                pass

    # override to add more shortcuts; menu items are used as a convenient way to
    # store shortcuts data; they are never used as menu; items may have 'target'
    # or 'method' params which point to the function/method to be called; the
    # difference is that methods will receive an object as parameter when they
    # are called
    def get_shortcuts(cls):
        menu = ui.Menu()
        menu.append(ui.MenuItem(_('New'), target=app.new_click))
        menu.append(ui.MenuItem(_('Open...'), target=app.open_click))
        menu.append(ui.MenuItem(_('Python Shell'), target=StdIOWrapper.shell))
        menu.append(ui.MenuItem(_('Run Script...'), target=app.runscript_click))
        menu.append(ui.MenuItem(_('Settings'), target=app.settings_click))
        menu.append(ui.MenuItem(_('Plugins'), target=app.plugins_click))
        menu.append(ui.MenuItem(_('Help'), target=app.help_click))
        menu.append(ui.MenuItem(_('Orientation'), target=app.orientation_click))
        menu.append(ui.MenuItem(_('Exit'), target=app.exit_click))
        menu.append(ui.MenuItem(_('File Menu'), method=cls.popup_file_menu))
        return menu
    get_shortcuts = classmethod(get_shortcuts)

    # turns the menu built by get_shortcuts() into a list of (name, item)
    # tuples where 'name' is a name generated by introspecting the 'target'
    # or 'method' params of items, 'item' is a ui.MenuItem object
    def get_shortcuts_items(cls):
        menu = cls.get_shortcuts().copy()
        menu.sort()
        choices = []
        names = []
        for item in menu:
            try:
                target = item.target
            except AttributeError:
                try:
                    target = item.method
                except AttributeError:
                    continue
            name = target.__name__
            if name in names:
                raise TypeError('%s: shortcuts names conflict (%s and %s)' % \
                    (repr(name), repr(dict(choices)[name]), repr(item)))
            choices.append((name, item))
            names.append(name)
        return choices
    get_shortcuts_items = classmethod(get_shortcuts_items)

    # sets a shortcut for this window, 'key' is a key identifier, 'item' is
    # a ui.MenuItem object with 'target' or 'method' params
    def set_shortcut(self, key, item):
        if item is not None:
            if key not in self.control_keys:
                self.control_keys += (key,)
            self.shortcuts[key] = item
        else:
            if key is self.control_keys:
                keys = list(self.control_keys)
                keys.remove(key)
                self.control_keys = keys
            try:
                del self.shortcuts[key]
            except KeyError:
                pass

    def control_key_press(self, key):
        try:
            item = self.shortcuts[key]
        except KeyError:
            return False
        try:
            target = item.target
        except AttributeError:
            pass
        else:
            target()
        try:
            target = item.method
        except AttributeError:
            pass
        else:
            target(self)
        return True

    def popup_file_menu(self):
        item = self.menu.find(title=_('File'))[0].submenu.popup()
        if item:
            item.target()
        self.reset_control_key()

    def apply_settings(self):
        self.set_shortcuts()

    # sets all shortcuts defined in app.settings.main.shortcuts
    def set_shortcuts(self):
        items = dict(self.get_shortcuts_items())
        for key, val in app.settings.main.shortcuts.items():
            if val:
                try:
                    item = items[val]
                except KeyError:
                    self.set_shortcut(key, None)
                else:
                    self.set_shortcut(key, items[val])
            else:
                self.set_shortcut(key, None)

    def update_settings(cls):
        try:
            ui.screen.rootwin.focus = True
        except AttributeError:
            pass
        for win in ui.screen.find_windows(GlobalWindowModifier):
            win.apply_settings()
        try:
            ui.screen.rootwin.focus = False
        except AttributeError:
            pass
    update_settings = classmethod(update_settings)


class RootWindow(ui.RootWindow, GlobalWindowModifier):
    def __init__(self, **kwargs):
        ui.RootWindow.__init__(self, **kwargs)
        GlobalWindowModifier.__init__(self)
        self.no_popup_menu = False
        self.keys += (ui.EKeySelect,)
        self.text = _('Version: %s\nPython for S60: %s\n') % (__version__, e32.pys60_version)
        
        # setup stdio redirection
        self.old_stdio = sys.stdin, sys.stdout, sys.stderr
        sys.stdin = sys.stdout = sys.stderr = StdIOWrapper()

    def open(self, focus=True):
        GlobalWindowModifier.open(self)
        ui.RootWindow.open(self, focus)

    def redraw_callback(self, rect):
        ui.RootWindow.redraw_callback(self, rect)
        if len(ui.screen.windows) == 1: # if the root window is the only one
            white = 0xffffff
            if e32.pys60_version_info[:3] >= (1, 3, 22):
                font = ('dense', 12)
            else:
                font = 'dense'
            space = 8
            m = self.body.measure_text(u'A', font=font)[0]
            h = m[3]-m[1]
            x, y = 10, 10+h
            self.body.text((x, y), u'Ped - Python IDE', fill=white, font=font)
            y += space
            self.body.line((x, y, ui.layout(ui.EMainPane)[0][0]-x, y), outline=white)
            for ln in unicode(self.text).split(u'\n'):
                ln = ln.strip()
                y += space+h
                self.body.text((x, y), ln, fill=white, font=font)

    def close(self):
        r = ui.RootWindow.close(self)
        if r:
            self.shutdown()
    
    def shutdown(self):
        # restore stdio redirection
        sys.stdin, sys.stdout, sys.stderr = self.old_stdio
        # exit application
        ui.app.set_exit()

    def key_press(self, key):
        if key == ui.EKeySelect:
            if self.no_popup_menu:
                return
            menu = ui.Menu(_('File'))
            menu.append(ui.MenuItem(_('Open...'), target=app.open_click))
            def make_target(klass):
                return lambda: app.new_file(klass)
            for klass in file_windows_types:
                menu.append(ui.MenuItem(_('New %s') % klass.type_name, target=make_target(klass)))
            item = menu.popup()
            if item:
                target = item.target
                # we disable the popup menu so it can't be immediately called again
                self.no_popup_menu = True
                def do():
                    target()
                    self.no_popup_menu = False
                ui.schedule(do)
        else:
            ui.RootWindow.key_press(self, key)

    def focus_changed(self, focus):
        GlobalWindowModifier.focus_changed(self, focus)
        ui.RootWindow.focus_changed(self, focus)

    def control_key_press(self, key):
        if GlobalWindowModifier.control_key_press(self, key):
            return True
        return ui.RootWindow.control_key_press(self, key)


class Window(ui.Window, GlobalWindowModifier):
    def __init__(self, **kwargs):
        ui.Window.__init__(self, **kwargs)
        GlobalWindowModifier.__init__(self)
        try:
            self.menu = ui.screen.rootwin.menu.copy()
        except AttributeError:
            # in case shell is opened to display an error while Ped is closing
            pass
            
    def open(self, focus=True):
        GlobalWindowModifier.open(self)
        ui.Window.open(self, focus)

    def focus_changed(self, focus):
        GlobalWindowModifier.focus_changed(self, focus)
        ui.Window.focus_changed(self, focus)

    def control_key_press(self, key):
        if GlobalWindowModifier.control_key_press(self, key):
            return True
        return ui.Window.control_key_press(self, key)


class TextWindow(Window):
    def __init__(self, **kwargs):
        Window.__init__(self, **kwargs)
        self.body = ui.Text()
        self.find_text = u''
        self.keys += (ui.EKeyEnter, ui.EKeySelect, ui.EKeyHome)
        self.control_keys += (ui.EKeyLeftArrow, ui.EKeyRightArrow,
                              ui.EKeyUpArrow, ui.EKeyDownArrow, ui.EKeyEdit)
        edit_menu = ui.Menu(_('Edit'))
        edit_menu.append(ui.MenuItem(_('Find...'), target=self.find_click))
        edit_menu.append(ui.MenuItem(_('Find Next'), target=self.findnext_click))
        edit_menu.append(ui.MenuItem(_('Find All...'), target=self.findall_click))
        edit_menu.append(ui.MenuItem(_('Go to Line...'), target=self.gotoline_click))
        edit_menu.append(ui.MenuItem(_('Top'), target=self.move_beg_of_document))
        edit_menu.append(ui.MenuItem(_('Bottom'), target=self.move_end_of_document))
        edit_item = ui.MenuItem(_('Edit'), submenu=edit_menu)
        try:
            file_item = self.menu.find(title=_('File'))[0]
        except IndexError:
            self.menu.insert(0, edit_item)
        else:
            self.menu.insert(self.menu.index(file_item)+1, edit_item)
        fullscreen_item = ui.MenuItem(_('Full Screen'), target=self.fullscreen_click)
        try:
            tools_menu = self.menu.find(title=_('Tools'))[0].submenu
        except IndexError:
            # shouldn't happen but what the heck :)
            edit_menu.append(fullscreen_item)
        else:
            tools_menu.append(fullscreen_item)

    def enter_key_press(self):
        pass

    def key_press(self, key):
        if key == ui.EKeySelect:
            self.body.add(u'\n')
            ui.schedule(self.enter_key_press)
        elif key == ui.EKeyEnter:
            ui.schedule(self.enter_key_press)
        elif key == ui.EKeyHome:
            # keep the behavior of self.move_beg_of_line() if Home key (on an
            # external keyboard) is pressed
            self.move_beg_of_line(immediate=False)
        else:
            Window.key_press(self, key)

    def control_key_press(self, key):
        if key == ui.EKeyLeftArrow:
            self.move_beg_of_line(immediate=False)
            self.reset_control_key()
        elif key == ui.EKeyRightArrow:
            self.move_end_of_line(immediate=False)
            self.reset_control_key()
        elif key == ui.EKeyUpArrow:
            self.move_page_up()
            return True
        elif key == ui.EKeyDownArrow:
            self.move_page_down()
            return True
        elif key == ui.EKeyEdit:
            self.popup_edit_menu()
        else:
            return Window.control_key_press(self, key)
        return False

    def get_shortcuts(cls):
        menu = Window.get_shortcuts()
        menu.append(ui.MenuItem(_('Find...'), method=cls.find_click))
        menu.append(ui.MenuItem(_('Find Next'), method=cls.findnext_click))
        menu.append(ui.MenuItem(_('Find All...'), method=cls.findall_click))
        menu.append(ui.MenuItem(_('Go to Line...'), method=cls.gotoline_click))
        menu.append(ui.MenuItem(_('Top'), method=cls.move_beg_of_document))
        menu.append(ui.MenuItem(_('Bottom'), method=cls.move_end_of_document))
        menu.append(ui.MenuItem(_('Full Screen'), method=cls.fullscreen_click))
        menu.append(ui.MenuItem(_('Edit Menu'), method=cls.popup_edit_menu))
        return menu
    get_shortcuts = classmethod(get_shortcuts)

    def popup_edit_menu(self):
        item = self.menu.find(title=_('Edit'))[0].submenu.popup()
        if item:
            item.target()
        self.reset_control_key()

    def set_style(self, **kwargs):
        ofont = self.body.font
        try:
            ofont, osize, oflags = ofont
            extended = True
        except:
            osize, oflags = 0, 0
            extended = False
        do = False
        font = pop(kwargs, 'font', ofont)
        size = pop(kwargs, 'size', osize)
        if pop(kwargs, 'antialias', oflags & 16):
            flags = 16 # graphics.FONT_ANTIALIAS
        else:
            flags = 32 # graphics.FONT_NO_ANTIALIAS
        if pop(kwargs, 'bold', self.body.style & ui.STYLE_BOLD):
            style = ui.STYLE_BOLD
        else:
            style = 0
        color = pop(kwargs, 'color', self.body.color)
        # set new values
        if extended:
            self.body.font = (font, size, flags)
        else:
            self.body.font = font
        self.body.style = style
        self.body.color = color
        # refresh control
        pos = self.body.get_pos()
        self.body.set(self.body.get())
        self.body.set_pos(pos)
        if kwargs:
            raise TypeError('TextWindow.set_style() got an unexpected keyword argument(s): %s' % \
                ', '.join([repr(x) for x in kwargs.keys()]))

    def apply_settings(self):
        self.set_style(font=app.settings.text.fontname,
            size=app.settings.text.fontsize,
            antialias=app.settings.text.fontantialias,
            bold=app.settings.text.fontbold,
            color=app.settings.text.fontcolor)
        self.set_shortcuts()
    
    def set_shortcuts(self):
        Window.set_shortcuts(self)
        items = dict(self.get_shortcuts_items())
        for key, val in app.settings.text.shortcuts.items():
            if val:
                try:
                    item = items[val]
                except KeyError:
                    self.set_shortcut(key, None)
                else:
                    self.set_shortcut(key, items[val])
            else:
                self.set_shortcut(key, None)

    # returns all lines as list of (number, offset, string) tuples;
    # line numbers are counted from 1, offset is counted from start of text,
    # string is unicode
    def get_lines(self):
        lines = []
        pos = 0
        n = 1
        lst = self.body.get().splitlines()
        if not lst:
            lst.append(u'')
        for line in lst:
            lines.append((n, pos, line))
            n += 1
            pos += len(line) + 1
        return lines

    # returns a line based on given pos (offset); a line is a tuple
    # as returned by get_lines()
    def get_line_from_pos(self, pos=None, lines=None):
        if pos is None:
            pos = self.body.get_pos()
        if lines is None:
            lines = self.get_lines()
        for ln, lpos, line in lines:
            if lpos <= pos <= lpos+len(line):
                break
        return (ln, lpos, line)

    def find_click(self):
        find_text = ui.query(_('Find:'), 'text', self.find_text)
        if find_text:
            self.find_text = find_text
            self.findnext_click(False)
        self.reset_control_key()

    def findnext_click(self, skip=True):
        find_text = self.find_text.lower()
        if not find_text:
            self.find_click()
            return
        text = self.body.get().lower()
        i = self.body.get_pos()
        while True:
            pos = i
            if skip and text[i:i+len(find_text)] == find_text:
                i += len(find_text)
            i = text.find(find_text, i)
            if i >= 0:
                self.body.set_pos(i)
            else:
                if pos != 0:
                    if ui.query(_('Not found, start from beginning?'), 'query'):
                        i = 0
                        skip = False
                        continue
                else:
                    ui.note(_('Not found'))
            break
        self.reset_control_key()

    def findall_click(self):
        find_text = ui.query(_('Find All:'), 'text', self.find_text)
        if find_text:
            self.find_text = find_text
            find_text = find_text.lower()
            results = []
            for ln, lpos, line in self.get_lines():
                pos = 0
                while 1:
                    pos = line.lower().find(find_text, pos)
                    if pos < 0:
                        break
                    results.append((ln, lpos, line, pos))
                    pos += len(find_text)
            if results:
                win = FindResultsWindow(title=_('Find: %s') % find_text,
                    results=results)
                line = win.modal(self)
                if line:
                    self.body.set_pos(line[1] + line[3])
            else:
                ui.note(_('Not found'))
        self.reset_control_key()

    def gotoline_click(self):
        lines = self.get_lines()
        ln = self.get_line_from_pos(lines=lines)[0]
        ln = ui.query(_('Line (1-%d):') % len(lines), 'number', ln)
        if ln is not None:
            if ln < 1:
                ln = 1
            ln -= 1
            try:
                self.body.set_pos(lines[ln][1])
            except IndexError:
                self.body.set_pos(self.body.len())
        self.reset_control_key()

    def set_pos(self, pos, immediate=True):
        if immediate:
            self.body.set_pos(pos)
        else:
            ui.schedule(self.body.set_pos, pos)

    def move_beg_of_line(self, immediate=True, force=False):
        pos = self.body.get_pos()
        lnum, offset, ln = self.get_line_from_pos(pos)
        if force:
            indent = 0
        else:
            # first jump to the beginning of text in a line, then to the first char
            try:
                indent = ln.index(ln.lstrip()[0])
            except:
                indent = 0
            if indent == pos - offset:
                indent = 0
        self.set_pos(offset + indent, immediate)

    def move_end_of_line(self, immediate=True):
        line = self.get_line_from_pos()
        self.set_pos(line[1] + len(line[2]), immediate)

    def get_pagesize(self):
        sett = 'pagesize'
        if self.orientation == ui.oriAutomatic:
            w, h = ui.layout(ui.EApplicationWindow)[0]
            if w > h:
                sett += 'land'
            else:
                sett += 'port'
        elif self.orientation == ui.oriLandscape:
            sett += 'land'
        else:
            sett = 'port'
        if self.size == ui.sizLarge:
            sett = sett[:-4] + 'full'
        return app.settings.text[sett].get()

    def move_page_up(self):
        self.move_line_up(count=self.get_pagesize())

    def move_page_down(self):
        self.move_line_down(count=self.get_pagesize())

    def move_line_up(self, count=1):
        lines = self.get_lines()
        pos = self.body.get_pos()
        ln, lpos, line = self.get_line_from_pos(lines=lines, pos=pos)
        i = ln-1-count
        if i < 0:
            i = 0
        lpos = pos-lpos
        if lpos > len(lines[i][2]):
            lpos = len(lines[i][2])
        self.set_pos(lines[i][1]+lpos)

    def move_line_down(self, count=1):
        lines = self.get_lines()
        pos = self.body.get_pos()
        ln, lpos, line = self.get_line_from_pos(lines=lines, pos=pos)
        i = ln-1+count
        if i >= len(lines):
            i = -1
        lpos = pos-lpos
        if lpos > len(lines[i][2]):
            lpos = len(lines[i][2])
        self.set_pos(lines[i][1]+lpos)

    def move_beg_of_document(self):
        self.set_pos(0)
        self.reset_control_key()

    def move_end_of_document(self):
        self.set_pos(self.body.len())
        self.reset_control_key()

    def reset_caret(self):
        self.body.set_pos(self.body.get_pos())

    def fullscreen_click(self):
        if self.size == ui.sizNormal:
            self.size = ui.sizLarge
        else:
            self.size = ui.sizNormal
        self.reset_control_key()


class FindResultsWindow(Window):
    def __init__(self, **kwargs):
        self.results = pop(kwargs, 'results')
        kwargs.setdefault('title', _('Find All'))
        Window.__init__(self, **kwargs)
        self.body = ui.Listbox([(_('Line %d, Column %d') % (x[0], x[3]), x[2]) for x in self.results], self.select_click)
        self.menu = ui.Menu()
        self.menu.append(ui.MenuItem(_('Select'), target=self.select_click))
        self.menu.append(ui.MenuItem(_('Exit'), target=self.close))

    def select_click(self):
        self.modal_result = self.results[self.body.current()]
        self.close()


class TextFileWindow(TextWindow):
    type_name = 'Text'
    type_ext = '.txt'
    session = ui.SettingsGroup()
    session.append('windows', ui.Setting('', []))

    def __init__(self, **kwargs):
        try:
            self.path = pop(kwargs, 'path')
        except KeyError:
            self.path = None
            self.fixed_encoding = False
            self.encoding = 'latin1'
            TextWindow.__init__(self, **kwargs)
        else:
            text, self.encoding = self.load()
            self.fixed_encoding = True
            TextWindow.__init__(self, **kwargs)
            self.body.set(text)
            self.body.set_pos(0)
            self.title = os.path.split(self.path)[1].decode('utf8')
        self.autosave_timer = e32.Ao_timer()
        file_menu = self.menu.find(title=_('File'))[0].submenu
        file_menu.append(ui.MenuItem(_('Save'), target=self.save))
        file_menu.append(ui.MenuItem(_('Save As...'), target=self.save_as))
        file_menu.append(ui.MenuItem(_('Save All'), target=self.save_all))
        file_menu.append(ui.MenuItem(_('Close'), target=self.close))
        file_menu.append(ui.MenuItem(_('Close All'), target=self.close_all))

    def apply_settings(self):
        TextWindow.apply_settings(self)
        try:
            if not self.fixed_encoding:
                self.encoding = app.settings.file.encoding
            autosave = app.settings.file.autosave
            self.autosave_timer.cancel()
            if autosave and self.path is not None:
                self.autosave_timer.after(autosave, self.autosave)
        except AttributeError:
            pass

    def can_close(self):
        if not TextWindow.can_close(self):
            return False
        text = self.body.get()
        if self.path is None:
            if not text:
                return True
        else:
            try:
                if text == self.load()[0]:
                    return True
            except IOError:
                pass
        menu = ui.Menu(_('Changes'))
        menu.append(ui.MenuItem(_('Save'), value=True))
        menu.append(ui.MenuItem(_('Discard'), value=False))
        item = menu.popup()
        if item:
            if item.value:
                return self.save()
            else:
                return True
        return False

    def close(self):
        if TextWindow.close(self):
            self.autosave_timer.cancel()
            return True
        return False

    def get_shortcuts(cls):
        menu = TextWindow.get_shortcuts()
        menu.append(ui.MenuItem(_('Save'), method=cls.save_and_popup))
        menu.append(ui.MenuItem(_('Save As...'), method=cls.save_as))
        menu.append(ui.MenuItem(_('Save All'), method=cls.save_all))
        menu.append(ui.MenuItem(_('Close'), method=cls.close))
        menu.append(ui.MenuItem(_('Close All'), method=cls.close_all))
        return menu
    get_shortcuts = classmethod(get_shortcuts)

    def save_and_popup(self):
        if self.save():
            if hasattr(ui, 'infopopup'):
                ui.infopopup.show(_('File saved'))
            else:
                ui.note(_('File saved'))
            return True
        return False

    def load(self):
        if self.path is None:
            raise IOError('TextFileWindow: no path specified')
        f = file(self.path, 'r')
        text = f.read()
        f.close()
        if text.startswith('\xff\xfe') or text.startswith('\xfe\xff'):
            enc = 'utf16'
            text = text.decode(enc)
        else:
            for enc in ['utf8', 'latin1']:
                try:
                    text = text.decode(enc)
                    break
                except UnicodeError:
                    pass
            else:
                raise UnicodeError
        return text.translate({13: None, 10: 0x2029}), enc

    def save(self):
        if self.path is None:
            return self.save_as()
        autosave = app.settings.file.autosave
        self.autosave_timer.cancel()
        if autosave:
            self.autosave_timer.after(autosave, self.autosave)
        try:
            f = file(self.path, 'w')
            f.write(self.body.get().translate({0x2028: 0x2029, 0xa0: 0x20}).replace(u'\u2029',
                u'\r\n').encode(self.encoding))
            f.close()
            return True
        except IOError:
            ui.note(_('Cannot save file'), 'error')
            return False

    def autosave(self):
        if self.save():
            if hasattr(ui, 'infopopup'):
                ui.infopopup.show(_('File saved'))

    def save_as(self):
        path = self.path
        if path is None:
            path = self.title.encode('utf8')
        win = ui.FileBrowserWindow(mode=ui.fbmSave,
            path=path,
            title=_('Save file'))
        path = win.modal(self)
        if path is None:
            return False
        self.path = path
        self.title = os.path.split(path)[1].decode('utf8')
        ret = self.save()
        if ret and os.path.splitext(path)[-1].lower() != self.type_ext.lower():
            # extension changed, reload file to create appropriate window object
            pos = self.body.get_pos()
            self.close()
            win = app.load_file(path)
            if win:
                win.body.set_pos(pos)
        return ret

    def close_all(self):
        ui.screen.close_windows(TextFileWindow)

    def save_all(self):
        for win in ui.screen.find_windows(TextFileWindow):
            if not win.save():
                return

    def store_session(cls):
        windows = cls.session.windows
        del windows[:]
        for win in ui.screen.find_windows(TextFileWindow):
            try:
                text = win.body.get()
                encoding = win.encoding
                if win.load()[0] == text:
                    # file unchanged - no need to store the text
                    text = None
                else:
                    raise IOError
            except IOError:
                pass
            if win.path:
                path = win.path
            else:
                path = win.title.encode('utf8')
            windows.insert(0, (path, text, encoding, win.body.get_pos()))
        cls.session.save()
    store_session = classmethod(store_session)

    def clear_session(cls):
        del cls.session.windows[:]
        cls.session.save()
    clear_session = classmethod(clear_session)


class AutocloseTextWindow(TextWindow):
    def focus_changed(self, focus):
        if not focus:
            self.close()


class PythonModifier(object):
    py_namespace = {}

    def __init__(self):
        edit_menu = self.menu.find(title=_('Edit'))[0].submenu
        edit_menu.append(ui.MenuItem(_('Call Tip'), target=self.py_calltip))

    def py_reset_namespace(cls):
        import __main__
        cls.py_namespace.clear()
        cls.py_namespace.update(__main__.__dict__)
        cls.py_namespace.update(__main__.__builtins__.__dict__)
        cls.py_namespace['__name__'] = '__main__'
    py_reset_namespace = classmethod(py_reset_namespace)

    def _get_text(self):
        return self.body.get(), self.body.get_pos()

    def _get_objects(self, exp):
        i = exp.rfind('.')
        if i > 0:
            exp, limit = exp[:i], exp[i+1:]
        elif i == 0: # expression cannot start with a dot
            return {}
        else:
            exp, limit = '', exp
        limit = limit.strip()
        if exp:
            # expression
            namespace = sys.modules.copy()
            namespace.update(self.py_namespace)
            try:
                d = [x for x in eval('dir(%s)' % exp, namespace) if x.startswith(limit)]
                return dict([(x, eval('%s.%s' % (exp, x), namespace)) for x in d])
            except:
                pass
        else:
            # globals
            try:
                d = [x for x in eval('dir()', self.py_namespace) if x.startswith(limit)]
                return dict([(x, eval(x, self.py_namespace)) for x in d])
            except:
                pass
        return {}

    def _get_object(self, exp):
        d = self._get_objects(exp)
        try:
            # if rfind() returns -1 then we will get whole <exp>
            return d[exp[exp.rfind('.')+1:]]
        except:
            pass

    def _get_expression(self, text=None, pos=None):
        if text is None or pos is None:
            ttext, tpos = self._get_text()
            if text is None:
                text = ttext
            if pos is None:
                pos = tpos
        begpos = pos

        brackets = {u'(': u')', u'[': u']', u'{': u'}'}
        brstack = []
        quote = u''
        
        # parse back to get the expression
        pos -= 1; lastc = lastnwc = ''
        while pos >= 0:
            c = text[pos]
            if c in u'"\'': # eat everything inside quotes
                if not quote:
                    quote = c
                elif c == quote and (pos == 0 or text[pos-1] != u'\\'):
                    quote = ''
            elif not quote:
                if c in brackets.values(): # eat everything inside brackets
                    if lastnwc.isalnum() or lastnwc in (u'', u'_'):
                        # bracket followed by an id-char or cursor
                        break
                    brstack.append(c)
                elif c in brackets.keys():
                    if not brstack: # opening bracket, stop
                        break
                    if brstack.pop() != brackets[c]: # unmatched brackets, stop
                        break
                elif not brstack:
                    if c.isalnum() or c in u'._':
                        if lastc.isspace():
                            if lastnwc not in (u'', u'.') or c != u'.':
                                break
                    elif not c.isspace():
                        break
            lastc = c
            if not c.isspace():
                lastnwc = c
            pos -= 1
        pos += 1

        try:
            return text[pos:begpos].lstrip().translate({0x2028: 0x2029, 0xa0: 0x20}).replace(u'\u2029',
                u'\n').encode('latin1')
        except UnicodeError:
            return ''

    def _expression_to_title(self, exp):
        r = exp.replace('\n', ' ')
        title = ''
        while title != r:
            title = r
            r = title.replace('  ', ' ')
        return title.strip().decode('latin1')

    def py_insert_indent(self):
        text, pos = self._get_text()
        pos -= 1
        i = pos-1

        # walk back to the start of line
        while i >= 0:
            if text[i] in (u'\u2028', u'\u2029'):
                break
            i -= 1
        i += 1
        strt = i

        # walk forward to the start of text
        while i < pos and text[i].isspace():
            i += 1

        # calculate indent of the previous line
        ind = i-strt
        if pos > 0:
            if text[pos-1] == u':':
                # add more indent if line ends with a colon
                ind += app.settings.python.indentsize
            else:
                # add more indent if line has unmatched brackets
                level = 0
                # i is already set here
                #i = strt+ind
                while i < pos:
                    if text[i] in u'([{':
                        level += 1
                    elif text[i] in u')]}':
                        level -= 1
                    i += 1
                if level > 0:
                    ind += app.settings.python.indentsize
        self.body.add(u' '*ind)

    def py_autocomplete(self):
        # parse back to get the expression
        exp = self._get_expression()

        # build the menu
        
        menu = ui.Menu('%s*' % self._expression_to_title(exp))
        menu.extend([ui.MenuItem(title) for title in self._get_objects(exp).keys()])
        menu.extend([ui.MenuItem(title, offset=off) for title, off in statements \
            if title.startswith(exp)])
        menu.sort()
        symbitems = [ui.MenuItem(title, offset=off) for title, off in symbols]
        if exp:
            menu.extend(symbitems)
        else:
            menu[:] = symbitems + menu
            
        # open the autocomplete list
        item = menu.popup(full_screen=True, search_field=True)
        if item is not None:
            # insert the selected object
            ws = s = unicode(item.title)
            n = exp.split(u'.')[-1]
            if s.startswith(n):
                s = s[len(n):]
            self.body.add(s)
            if hasattr(item, 'offset'): # statement, symbol
                if item.offset is not None:
                    self.body.set_pos(self.body.get_pos() - len(ws) + item.offset)

    def py_calltip(self):
        stdhelp = _('Put cursor inside argument parenthesis')
        text, pos = self._get_text()
        
        # search back to opening bracket
        pos -= 1
        lev = 0
        while pos >= 0:
            if text[pos] == u'(':
                lev -= 1
                if lev < 0:
                    break
            elif text[pos] == u')':
                lev += 1
            pos -= 1
        else:
            ui.note(stdhelp)
            return
        
        # search back to non-space chars
        while pos >= 0:
            if not text[pos].isspace():
                break
            pos -= 1
        else:
            ui.note(stdhelp)
            return
        
        # extract the expression
        exp = self._get_expression(text, pos)
        
        if exp:
            title = self._expression_to_title(exp)
            try:
                from globalui import global_msg_query
                win = None
            except ImportError:
                win = AutocloseTextWindow(title=u'%s - %s' % (_('Call Tip'), title))
                menu = ui.Menu()
                menu.append(ui.MenuItem(_('Close'), target=win.close))
                win.menu = menu
            # try to get the object
            obj = self._get_object(exp)
            if obj is not None:
                # check the type of the object and try to obtain the function object
                import types
                argoffset = 0
                arg_text = ''
                if type(obj) in (types.ClassType, types.TypeType):
                    def find_init(obj):
                        try:
                            return obj.__init__.im_func
                        except AttributeError:
                            for base in obj.__bases__:
                                fob = find_init(base)
                                if fob is not None:
                                    return None
                    fob = find_init(obj)
                    if fob is None:
                        fob = lambda: None
                    else:
                        argoffset = 1
                elif type(obj) == types.MethodType:
                    fob = obj.im_func
                    argoffset = 1
                else:
                    fob = obj
                if type(fob) in (types.FunctionType, types.LambdaType):
                    try:
                        real_args = fob.func_code.co_varnames[argoffset:fob.func_code.co_argcount]
                        defaults = fob.func_defaults or []
                        defaults = list(['=%s' % repr(x) for x in defaults])
                        defaults = [''] * (len(real_args) - len(defaults)) + defaults
                        items = map(lambda arg, dflt: arg+dflt, real_args, defaults)
                        if fob.func_code.co_flags & 0x4:
                            items.append('...')
                        if fob.func_code.co_flags & 0x8:
                            items.append('***')
                        arg_text = '%s(%s)' % (title, ', '.join(items))
                    except:
                        pass
                doc = getattr(obj, '__doc__', '')
                if doc:
                    while doc[:1] in ' \t\n':
                        doc = doc[1:]
                    if not arg_text:
                        arg_text = title
                    arg_text += '\n\n' + doc
                if arg_text:
                    text = unicode(arg_text)
                    # display the call-tip
                    if win:
                        win.body.add(text + u'\n')
                    else:
                        # we use a timer to let the screen refresh if we were called
                        # from a shortcut; if we won't do it, the Text control text
                        # will not be visible while the popup remains open
                        timer = e32.Ao_timer()
                        def timerhandler(timer):
                            global_msg_query(text, u'%s - %s' % (_('Call Tip'), title))
                        timer.after(0.0, lambda: timerhandler(timer))
                else:
                    if win:
                        win.close()
                    ui.note(_('No additional info for "%s"') % title)
            else:
                if win:
                    win.close()
                ui.note(_('Unknown callable "%s"') % title)
            if win and not win.is_closed():
                win.body.set_pos(0)
                win.open()
        else:
            ui.note(stdhelp)
            
    def py_calltip_scheduled(self):
        ui.schedule(self.py_calltip)

    def get_shortcuts(cls):
        menu = ui.Menu()
        menu.append(ui.MenuItem(_('Call Tip'), method=cls.py_calltip_scheduled))
        menu.append(ui.MenuItem(_('Autocomplete'), method=cls.py_autocomplete))
        return menu
    get_shortcuts = classmethod(get_shortcuts)


PythonModifier.py_reset_namespace()


class PythonCodeBrowserWindow(Window, ui.FilteredListboxModifier):
    def __init__(self, **kwargs):
        self.tree = pop(kwargs, 'tree')
        kwargs.setdefault('title', '__main__')
        Window.__init__(self, **kwargs)
        ui.FilteredListboxModifier.__init__(self, _('(no match)'))
        self.set_listbox(self.make_display_list(), self.click)
        self.menu = ui.Menu()
        self.menu.append(ui.MenuItem(_('Select'), target=self.select_click))
        self.menu.append(ui.MenuItem(_('Browse'), target=self.browse_click))
        self.menu.append(ui.MenuItem(_('Back'), target=self.back_click))
        self.menu.append(self.filter_menu_item)
        self.menu.append(ui.MenuItem(_('Exit'), target=self.close))
        self.keys += (ui.EKeyLeftArrow, ui.EKeyRightArrow)
        self.stack = []
        
    def make_display_list(self):
        lst = []
        for name in self.make_list():
            if self.tree[name][-1]:
                name += ' ->'
            lst.append(name)
        if not lst:
            lst.append(_('(no data)'))
        return lst
        
    def make_list(self):
        lst = self.tree.keys()
        lst.sort(lambda a, b: -(a.lower() < b.lower()))
        return lst

    def key_press(self, key):
        if key == ui.EKeyLeftArrow:
            self.back_click()
        elif key == ui.EKeyRightArrow:
            self.browse_click()
        else:
            Window.key_press(self, key)
            ui.FilteredListboxModifier.key_press(self, key)

    def current_name(self):
        i = self.current()
        if i < 0:
            return
        lst = self.make_list()
        try:
            return lst[i]
        except IndexError:
            return

    def click(self):
        name = self.current_name()
        if name is None:
            return
        try:
            newtree = self.tree[name][-1]
        except KeyError:
            return
        menu = ui.Menu()
        menu.append(self.menu[0]) # Select
        if newtree:
            menu.append(self.menu[1]) # Browse
        item = menu.popup()
        if item is not None:
            item.target()

    def select_click(self):
        name = self.current_name()
        if name is None:
            return
        try:
            self.modal_result = self.tree[name][0]
        except KeyError:
            return
        self.close()
        
    def back_click(self):
        oldtree = self.tree
        try:
            self.tree = self.stack.pop()
        except IndexError:
            return
        pos = [self.tree[name][-1] for name in self.make_list()].index(oldtree)
        self.set_filter(None)
        self.filter_title = self.filter_title[:self.title.rindex(u'.')]
        self.set_list(self.make_display_list(), pos)
        
    def browse_click(self):
        name = self.current_name()
        if name is None:
            return
        try:
            newtree = self.tree[name][-1]
        except KeyError:
            return
        if not newtree:
            return
        self.stack.append(self.tree)
        self.tree = newtree
        if name.endswith('()'):
            name = name[:-2]
        self.set_filter(None)
        self.filter_title += u'.%s' % name
        self.set_list(self.make_display_list(), 0)


class PythonFileWindow(TextFileWindow, PythonModifier):
    type_name = 'Python'
    type_ext = '.py'

    def __init__(self, **kwargs):
        TextFileWindow.__init__(self, **kwargs)
        PythonModifier.__init__(self)
        self.control_keys += (ui.EKeySelect,)
        self.args = u''
        self.menu.insert(0, ui.MenuItem(_('Run'), target=self.run_click))
        edit_menu = self.menu.find(title=_('Edit'))[0].submenu
        edit_menu.append(ui.MenuItem(_('Code Browser'), target=self.codebrowser_click))

    def enter_key_press(self):
        TextFileWindow.enter_key_press(self)
        self.py_insert_indent()

    def control_key_press(self, key):
        if key == ui.EKeySelect:
            self.py_autocomplete()
            self.reset_control_key()
            return False
        return TextFileWindow.control_key_press(self, key)

    def get_shortcuts(cls):
        menu = TextFileWindow.get_shortcuts()
        menu.extend(PythonModifier.get_shortcuts())
        menu.append(ui.MenuItem(_('Run'), method=cls.run_click))
        menu.append(ui.MenuItem(_('Code Browser'), method=cls.codebrowser_click))
        return menu
    get_shortcuts = classmethod(get_shortcuts)

    def run_click(self):
        TextFileWindow.store_session()
        try:
            if self.load()[0] == self.body.get():
                path = self.path
            else:
                raise IOError
        except IOError:
            # save to temp file
            dirpath = 'D:\\Ped.temp'
            if not os.path.exists(dirpath):
                try:
                    os.mkdir(dirpath)
                except OSError:
                    dirpath = 'D:\\'
            path = os.path.join(dirpath, self.title.encode('utf8'))
            try:
                f = file(path, 'w')
                f.write(self.body.get().translate({0x2028: 0x2029, 0xa0: 0x20}).replace(u'\u2029',
                    u'\r\n').encode(self.encoding))
                f.close()
            except IOError, (errno, errstr):
                ui.note(unicode(errstr), 'error')
                return
        if app.settings.python.askforargs:
            menu = ui.Menu(_('Arguments'))
            if self.args:
                menu.append(ui.MenuItem(_('Last: %s') % self.args, args=self.args))
            menu.append(ui.MenuItem(_('Edit...'), args=self.args, edit=True))
            menu.append(ui.MenuItem(_('No arguments'), args=u''))
            item = menu.popup()
            if not item:
                return
            if getattr(item, 'edit', False):
                args = ui.query(_('Arguments:'), 'text', item.args)
                if not args:
                    # cancel
                    return
                item.args = args
            self.args = item.args
            args = quote_split(self.args.encode('utf8'))
        else:
            self.args = u''
            args = []
        shell = StdIOWrapper.shell()
        if shell.is_busy():
            return
        shell.restart()
        shell.enable_prompt(False)
        shell.lock(True)
        from linecache import checkcache
        checkcache()
        # list() will make copies so we will be able to restore these later
        mysys = list(sys.argv), list(sys.path), dict(sys.modules)
        sys.path.insert(0, os.path.split(path)[0])
        sys.argv = [path] + args
        modules = sys.modules.keys()
        try:
            execfile(path, self.py_namespace)
        except:
            value, traceback_ = sys.exc_info()[1:]
            import traceback
            traceback.print_exc()
            e = traceback.extract_tb(traceback_)[-1]
            if e[0] != path:
                s = '(%s, line ' % os.path.split(path)[1]
                value = str(value)
                pos = value.find(s, 0)
                if pos >= 0:
                    value = value[pos + len(s):]
                    self.goto_error(int(value[:value.index(')')]))
            else:
                self.goto_error(e[1], unicode(e[3]))
            del traceback_
        for m in sys.modules.keys():
            if m not in modules:
                del sys.modules[m]
        sys.argv, sys.path, sys.modules = mysys
        shell = StdIOWrapper.shell()
        shell.lock(False)
        ui.screen.redraw()
        shell.enable_prompt(True)
        TextFileWindow.clear_session()
        def remove(name):
            if os.path.isdir(name):
                for item in os.listdir(name):
                    remove(os.path.join(name, item))
                os.rmdir(name)
            else:
                os.remove(name)
        remove('D:\\Ped.temp')

    def goto_error(self, lineno, text=None):
        ln, pos, line = self.get_lines()[lineno - 1]
        if text:
            c = line.find(text)
            if c > 0:
                pos += c
        self.body.set_pos(pos)

    def codebrowser_click(self):
        name = os.path.splitext(self.title)[0]
        win = PythonCodeBrowserWindow(title=name,
            tree=self.parse_lines())
        pos = win.modal(self)
        if pos is not None:
            self.body.set_pos(pos)

    def set_shortcuts(self):
        TextFileWindow.set_shortcuts(self)
        items = dict(self.get_shortcuts_items())
        for key, val in app.settings.python.fileshortcuts.items():
            if val:
                try:
                    item = items[val]
                except KeyError:
                    self.set_shortcut(key, None)
                else:
                    self.set_shortcut(key, items[val])
            else:
                self.set_shortcut(key, None)

    # parses Python code represented by lines (as returned by self.get_lines())
    # and returns a tree containing all classes and functions defined by this code in
    # following format:
    #   {name: (pos, {subname: (subpos, {})}), name2: (pos2, {})}
    # if name ends with '()' then it is a function
    def parse_lines(self, lines=None):
        if lines is None:
            lines = self.get_lines()
        flines = []
        idx = 0
        for lnum, lpos, ln in lines:
            t = ln.strip()
            if not t:
                continue
            ind = ln.find(t[0])
            flines.append((idx, ind, lpos, t))
            idx += 1
        end = {u'class' : u'', u'def' : u'()'}
        last = root = {}
        lev = [(0, root)] # indent, tree
        for idx, ind, lpos, ln in flines:
            t = ln.split()
            if ind < lev[-1][0]:
                if idx > 0:
                    if flines[idx-1][-1][-1] in (u',', u'\\'):
                        # current line is a continuation of previous line
                        flines[idx] = (idx, flines[idx-1][1], lpos, ln)
                        continue
                    if ind < flines[idx-1][1]:
                        pln = flines[idx-1][-1]
                        p = max(pln.find(u"'''"), pln.find(u'"""'))
                        if p >= 0 and p == max(pln.rfind(u"'''"), pln.rfind(u'"""')):
                            # current line is a continuation of multiline comment
                            flines[idx] = (idx, flines[idx-1][1], lpos, ln)
                            continue
                try:
                    while ind < lev[-1][0]:
                        lev.pop()
                except IndexError:
                    # error
                    return
            elif ind > lev[-1][0]:
                lev.append((ind, last))
            if t[0] in end.keys():
                tok = t[1].split(u'(')[0].split(u':')[0]
                name = tok+end[t[0]]
                if name in lev[-1][1]:
                    # duped names, append an integer
                    name = u'%s:%d%s' % (tok, lpos+ind, end[t[0]])
                last = {}
                lev[-1][1][name] = (lpos+ind, last)
        return root


class IOWindow(TextWindow):
    def __init__(self, **kwargs):
        TextWindow.__init__(self, **kwargs)
        self.control_keys += (ui.EKeyBackspace,)
        self.event = None
        self.locked = None
        self.write_buf = []
        def make_flusher(body, buf):
            def doflush():
                # insert the strings in place
                body.add(u''.join(buf))
                del buf[:]
                # while the len exceeds 3000 chars, we remove first 250
                while body.len() > 3000:
                    body.delete(0, 250)
                # update the Text object on screen
                e32.ao_yield()
            return doflush
        self.do_flush = make_flusher(self.body, self.write_buf)
        self.flush_gate = e32.ao_callgate(self.do_flush)

    def control_key_press(self, key):
        if key == ui.EKeyBackspace:
            if self.locked == False:
                # cause an KeyboardInterrupt in flush()
                self.interrupt()
            self.reset_control_key()
            return True
        else:
            return TextWindow.control_key_press(self, key)

    def enter_key_press(self):
        if self.event:
            self.event.set()
            return True
        TextWindow.enter_key_press(self)
        return False

    def can_close(self):
        if self.event:
            # readline() is in progress
            self.input_aborted = True
            self.write('\n')
            self.event.set()
            return False
        if self.is_locked():
            if self.is_interrupted():
                # user already interrupted the window but the interrupt status
                # wasn't processed, now he's trying again; ask him if he wants 
                # to close the app
                if ui.query(_('Kill unresponding window by closing Ped?'), 'query'):
                    # allow to close the window, see also: close()
                    return True
            # cause an KeyboardInterrupt in flush()
            self.interrupt()
            return False
        return TextWindow.can_close(self)

    def close(self):
        r = TextWindow.close(self)
        if r:
            # explicitly delete ao_callgate object to destroy circular reference
            del self.flush_gate
            if self.is_interrupted():
                # forcing a close (see: can_close())
                TextFileWindow.store_session()
                ui.screen.rootwin.shutdown()
        return r

    def lock(self, enable):
        if enable:
            self.locked = False
        else:
            self.locked = None

    def is_locked(self):
        return self.locked is not None

    def interrupt(self):
        self.locked = True
        if hasattr(ui, 'infopopup'):
            ui.infopopup.show(u'KeyboardInterrupt')

    def is_interrupted(self):
        return self.locked == True

    def readline(self, size=None):
        if not e32.is_ui_thread():
            raise IOError('IOWindow.readline() called from non-UI thread')
        self.input_aborted = False
        self.event = ui.Event()
        input_pos = self.body.get_pos()
        while not self.event.isSet():
            self.event.wait()
            if not self.input_aborted and self.body.get_pos() < input_pos:
                ln = self.body.len()
                self.body.set_pos(ln)
                if input_pos > ln:
                    input_pos = self.body.get_pos()
                continue
            break
        self.event = None
        if self.input_aborted:
            raise EOFError
        lst = self.body.get(input_pos).splitlines()
        if not lst:
            lst.append(u'')
        text = lst[0].encode('latin-1', 'replace') + '\n'
        self.body.set_pos(self.body.len())
        if size and len(text) > size:
            text = text[:size]
        return text

    def write(self, s):
        try:
            self.write_buf.append(unicode(s))
        except UnicodeError:
            self.write_buf.append(s.decode('latin1'))
        self.flush()

    def writelines(self, lines):
        self.write_buf += map(unicode, lines)
        self.flush()

    def flush(self):
        if self.write_buf:
            if e32.is_ui_thread():
                self.do_flush()
            else:
                self.flush_gate()
        if self.is_interrupted():
            self.lock(True)
            raise KeyboardInterrupt


class PythonShellWindow(IOWindow, PythonModifier):
    def __init__(self, **kwargs):
        kwargs.setdefault('title', _('Python Shell'))
        IOWindow.__init__(self, **kwargs)
        PythonModifier.__init__(self)
        self.control_keys += (ui.EKeyUpArrow, ui.EKeyDownArrow,
                              ui.EKeyBackspace, ui.EKeySelect)
        
        self.menu.insert(0, ui.MenuItem(_('History'), target=self.history_click))
        file_menu = self.menu.find(title=_('File'))[0].submenu
        file_menu.append(ui.MenuItem(_('Export To...'), target=self.export_click))
        edit_menu = self.menu.find(title=_('Edit'))[0].submenu
        edit_menu.append(ui.MenuItem(_('Clear'), target=self.clear_click))

        self.old_stdio = sys.stdin, sys.stdout, sys.stderr
        sys.stdin = sys.stdout = sys.stderr = self
        self.write('Python %s on %s\n' \
                   'Type "copyright", "credits" or "license" for more information.\n'
                   'Ped %s\n' % (sys.version, sys.platform, __version__))
        self.prompt_enabled = True
        self.init_console()
        self.prompt()

    def init_console(self):
        from code import InteractiveConsole
        self.console = InteractiveConsole(locals=self.py_namespace)
        self.history = [(u'import btconsole; btconsole.main()',)]
        self.history_ptr = len(self.history)
        try:
            sys.ps1
        except AttributeError:
            sys.ps1 = '>>> '

    def restart(self):
        PythonModifier.py_reset_namespace()
        try:
            del self.py_namespace['_']
        except KeyError:
            pass
        self.init_console()
        halfbar = '=' * 5
        self.move_end_of_document()
        self.write(halfbar + ' RESTART ' + halfbar + '\n')
        self.prompt()

    def close(self):
        r = IOWindow.close(self)
        if r:
            sys.stdin, sys.stdout, sys.stderr = self.old_stdio
        return r

    def control_key_press(self, key):
        if key in (ui.EKeyUpArrow, ui.EKeyDownArrow):
            if key == ui.EKeyUpArrow:
                # history back
                if self.history_ptr > 0:
                    self.history_ptr -= 1
                else:
                    ui.schedule(self.body.set_pos, self.body.get_pos())
                    return False
            else:
                # history forth
                if self.history_ptr < len(self.history):
                    self.history_ptr += 1
                else:
                    ui.schedule(self.body.set_pos, self.body.get_pos())
                    return False
            try:
                statement = self.history[self.history_ptr]
                try:
                    self.body.delete(self.prompt_pos)
                except SymbianError:
                    pass
                self.body.set_pos(self.prompt_pos)
                self.write('\n'.join(statement))
                ui.schedule(self.body.set_pos, self.body.get_pos())
            except IndexError:
                self.body.delete(self.prompt_pos)
                ui.schedule(self.body.set_pos, self.prompt_pos)
        elif key == ui.EKeyBackspace:
            if not self.is_locked():
                pos = self.body.get_pos()
                if pos >= self.prompt_pos:
                    if pos == self.prompt_pos:
                        self.body.add(u' ')
                    if self.body.len() > self.prompt_pos:
                        def clear():
                            self.body.delete(self.prompt_pos)
                            self.body.set_pos(self.prompt_pos)
                        ui.schedule(clear)
                        self.reset_control_key()
            else:
                return IOWindow.control_key_press(self, key)
        elif key == ui.EKeySelect:
            self.py_autocomplete()
            self.reset_control_key()
        else:
            return IOWindow.control_key_press(self, key)
        return False

    def get_shortcuts(cls):
        menu = IOWindow.get_shortcuts()
        menu.extend(PythonModifier.get_shortcuts())
        menu.append(ui.MenuItem(_('Clear'), method=cls.clear_click))
        return menu
    get_shortcuts = classmethod(get_shortcuts)

    def calltip_click(self):
        ui.schedule(self.py_calltip)

    def enable_prompt(self, enable):
        enabled = self.prompt_enabled
        self.prompt_enabled = bool(enable)
        if not enabled and enable:
            self.prompt()
        elif enabled and not enable:
            self.write('\n')

    def prompt(self):
        if not self.prompt_enabled:
            return
        try:
            self.write(str(sys.ps1))
        except:
            pass
        self.prompt_pos = self.body.get_pos()
        self.statement = []

    def enter_key_press(self):
        if IOWindow.enter_key_press(self):
            return
        if self.is_locked():
            return
        pos = self.body.get_pos()
        # remove new line character
        if pos > 0 and self.body.get(pos-1, 1) in (u'\u2028', u'\u2029'):
            self.body.delete(pos-1, 1)
            pos -= 1
        if pos < self.prompt_pos:
            # cursor was moved before the statement start
            self.body.set_pos(self.body.len())
            if self.body.get_pos() < self.prompt_pos:
                # prompt was deleted, issue a new one
                self.write('\n')
                self.prompt()
                return
            # recall a line
            line = self.get_line_from_pos(pos=pos)[2]
            try:
                if line.startswith(str(sys.ps1)):
                    line = line[len(str(sys.ps1)):]
            except:
                pass
            self.write(line)
            return
        if len(self.body.get(pos).splitlines()) > 1:
            # cursor was moved back in an multiline statement
            self.write('\n')
            self.py_insert_indent()
            return
        # cursor at the last statement line, statement execution follows
        self.body.set_pos(self.body.len())
        statement = self.body.get(self.prompt_pos).translate({0xa0: 0x20}).splitlines()
        if not statement:
            statement.append(u'')
        self.write('\n')
        # remove empty lines so they don't break the statement
        statement = [x for x in statement[:-1] if x.strip()] + statement[-1:]
        self.lock(True)
        try:
            self.console.resetbuffer()
            more = False
            for line in statement:
                if line.strip():
                    s = line
                else:
                    s = u''
                if not self.console.push(s.encode('latin1')):
                    break
            else:
                self.py_insert_indent()
                return
            if statement[0] and self.history[-1] != tuple(statement):
                self.history.append(tuple(statement))
            self.history_ptr = len(self.history)
            self.prompt()
        finally:
            self.lock(False)

    def apply_settings(self):
        self.set_style(font=app.settings.text.fontname,
            size=app.settings.text.fontsize,
            antialias=app.settings.text.fontantialias,
            bold=app.settings.text.fontbold,
            color=app.settings.python.shellfontcolor)
        self.set_shortcuts()
        
    def set_shortcuts(self):
        IOWindow.set_shortcuts(self)        
        items = dict(self.get_shortcuts_items())
        for key, val in app.settings.python.shellshortcuts.items():
            if val:
                try:
                    item = items[val]
                except KeyError:
                    self.set_shortcut(key, None)
                else:
                    self.set_shortcut(key, items[val])
            else:
                self.set_shortcut(key, None)

    def is_busy(self):
        if self.is_locked():
            ui.note(_('%s is busy') % self.title, 'error')
            return True
        return False

    def history_click(self):
        if self.is_busy():
            return
        win = HistoryWindow(history=self.history,
            ptr=self.history_ptr)
        ptr = win.modal(self)
        if ptr is not None:
            self.history_ptr = ptr
            statement = self.history[ptr]
            self.body.delete(self.prompt_pos)
            self.body.set_pos(self.prompt_pos)
            self.write('\n'.join(statement))

    def export_click(self):
        win = ui.FileBrowserWindow(mode=ui.fbmSave,
            path='PythonShell.txt',
            title=_('Export to'))
        path = win.modal(self)
        if path is None:
            return
        try:
            f = file(path, 'w')
            f.write(self.body.get().translate({0x2028: 0x2029, 0xa0: 0x20}).replace(u'\u2029',
                u'\r\n').encode(app.settings.file.encoding))
            f.close()
        except IOError:
            ui.note(_('Cannot export the output'), 'error')

    def clear_click(self):
        if ui.query(_('Clear the buffer?'), 'query'):
            self.body.clear()
            self.prompt()

    def move_beg_of_line(self, immediate=True, force=False):
        ln, pos, line = self.get_line_from_pos()
        # if we are in the prompt line, move to the start of prompt
        if pos <= self.prompt_pos <= pos + len(line):
            self.set_pos(self.prompt_pos, immediate)
        else:
            IOWindow.move_beg_of_line(self, immediate, force)


class HistoryWindow(Window):
    def __init__(self, **kwargs):
        self.history = pop(kwargs, 'history')
        ptr = pop(kwargs, 'ptr', 0)
        kwargs.setdefault('title', _('History'))
        Window.__init__(self, **kwargs)
        self.body = ui.Listbox([u''], self.select_click)
        self.body.set_list(['; '.join(filter(None, [y.strip() for y in x])).replace(':;', ':') for x in self.history], ptr)
        self.menu = ui.Menu()
        self.menu.append(ui.MenuItem(_('Select'), target=self.select_click))
        self.menu.append(ui.MenuItem(_('Exit'), target=self.close))

    def select_click(self):
        self.modal_result = self.body.current()
        self.close()


class HelpWindow(TextWindow):
    def __init__(self, **kwargs):
        text = pop(kwargs, 'text', None)
        path = pop(kwargs, 'path', None)
        head = pop(kwargs, 'head', u'')
        tail = pop(kwargs, 'tail', u'')
        kwargs.setdefault('title', _('Help'))
        TextWindow.__init__(self, **kwargs)
        if text is not None:
            text = unicode(text)
        elif path is not None:
            f = file(path, 'r')
            text = f.read().decode('utf8')
            f.close()
        else:
            raise TypeError('specify either \'text\' or \'path\' arguments')
        self.menu.insert(0, ui.MenuItem(_('Topic List'), target=self.topics_click))
        self.history = []
        self.topics = ui.Menu(_('Topic List'))
        stack = []
        lines = []
        text = ''.join((head, text, tail))
        offset = 0
        for ln in text.splitlines(True):
            if ln.startswith('$'): # topic
                title = ln.lstrip('$')
                level = len(ln)-len(title)
                title = title.strip()
                if level > len(stack):
                    stack.extend(['0']*(level-len(stack)))
                else:
                    stack = stack[:level]
                stack[-1] = str(int(stack[-1])+1)
                chapter = u'.'.join(stack)
                chaptit = u'%s. %s' % (chapter, title)
                self.topics.append(ui.MenuItem(chaptit, chapter=chapter,
                    topic=title, pos=offset))
                ln = u'%s\n' % chaptit
            lines.append(ln)
            offset += len(ln)
            if ln.endswith('\r\n'):
                offset -= 1
        self.body.set(''.join(lines))
        self.body.set_pos(0)

    def add_to_history(self, pos=None):
        if pos is None:
            pos = self.body.get_pos()
        if not self.history:
            self.menu.insert(0, ui.MenuItem(_('Back'), target=self.back_click))
            self.update_menu()
        self.history.append(pos)

    def enter_key_press(self):
        pos = self.body.get_pos()-1
        self.body.delete(pos, 1)
        lnum, offset, ln = self.get_line_from_pos(pos)
        pos -= offset
        br1 = ln.rfind(u'[', 0, pos)
        br2 = ln.find(u']', pos)
        if br1 >= 0 and br2 > 0:
            link = ln[br1+1:br2]
            items = self.topics.find(topic=link)
            if not items:
                items = self.topics.find(chapter=link)
                if not items:
                    items = self.topics.find(title=link)
            if items:
                self.add_to_history()
                self.body.set_pos(items[0].pos)

    def topics_click(self):
        item = self.topics.popup(search_field=True)
        if item:
            self.add_to_history()
            self.body.set_pos(item.pos)
            
    def back_click(self):
        try:
            pos = self.history.pop()
        except IndexError:
            pass
        else:
            self.body.set_pos(pos)
        if not self.history:
            items = self.menu.find(title=_('Back'))
            if items:
                self.menu.remove(items[0])
                self.update_menu()


class StdIOWrapper(object):
    singleton = None

    def __init__(self):
        assert self.singleton is None, 'only one instance of StdIOWrapper allowed'
        self.win = None
        StdIOWrapper.singleton = self

    def shell(cls):
        self = cls.singleton
        assert self, 'StdIOWrapper must be instatinated first'
        if self.win and self.win.is_opened():
            self.win.focus = True
            return self.win
        try:
            ui.screen.open_blank_window(_('Please wait...'))
            self.win = PythonShellWindow()
            self.win.open()
            return self.win
        except:
            # ui module failed, display the error using appuifw functions directly
            import traceback
            ui.app.title = u'Fatal Error'
            ui.app.screen = 'normal'
            ui.app.focus = None
            ui.app.body = ui.Text()
            lock = e32.Ao_lock()
            ui.app.exit_key_handler = lock.signal
            ui.app.menu = [(u'Exit', lock.signal)]
            ui.app.body.set(unicode(''.join(traceback.format_exception(*sys.exc_info()))))
            lock.wait()
            # restore ui module screen
            ui.screen.redraw()
            raise
    shell = classmethod(shell)

    def readline(self, size=None):
        return self.shell().readline(size)

    def write(self, s):
        return self.shell().write(s)

    def writelines(self, lines):
        return self.shell().writelines(lines)


class PluginsWindow(Window):
    def __init__(self, **kwargs):
        kwargs.setdefault('title', _('Plugins'))
        Window.__init__(self, **kwargs)
        self.plugins_path = os.path.join(app.path, 'plugins')
        self.body = ui.Listbox([(u'', u'')], self.select_click)
        self.body.bind(ui.EKeyBackspace, self.uninstall_click)
        self.menu_empty = ui.Menu()
        self.menu_empty.append(ui.MenuItem(_('Install...'), target=self.install_click))
        self.menu_empty.append(ui.MenuItem(_('Exit'), target=self.close))
        self.menu_plugins = ui.Menu()
        self.menu_plugins.append(ui.MenuItem(_('Install...'), target=self.install_click))
        self.menu_plugins.append(ui.MenuItem(_('Uninstall'), target=self.uninstall_click))
        self.menu_plugins.append(ui.MenuItem(_('Help'), target=self.help_click))
        self.menu_plugins.append(ui.MenuItem(_('Exit'), target=self.close))
        self.popup_menu_empty = ui.Menu()
        self.popup_menu_empty.append(ui.MenuItem(_('Install...'), target=self.install_click))
        self.popup_menu_plugins = ui.Menu()
        self.popup_menu_plugins.append(ui.MenuItem(_('Uninstall'), target=self.uninstall_click))
        self.popup_menu_plugins.append(ui.MenuItem(_('Help'), target=self.help_click))
        self.update()

    def update(self):
        plugins = []
        started = app.started_plugins.copy()
        if os.path.exists(self.plugins_path):
            for name in os.listdir(self.plugins_path):
                path = os.path.join(self.plugins_path, name)
                if not os.path.isdir(path):
                    continue
                try:
                    manifest = Manifest(os.path.join(path, 'manifest.txt'))
                except IOError:
                    continue
                plugins.append((path, name, manifest))
                if started.get(name, None) == (manifest['name'], manifest['version']):
                    del started[name]
        for name, info in started.items():
            plugins.append(('', name, {'name': info[0], 'version': info[1]}))
        plugins.sort(lambda a, b: \
            -(a[2]['name'].lower()+a[2]['version'] < \
            b[2]['name'].lower()+b[2]['version']))
        lst = []
        for path, name, manifest in plugins:
            if not path:
                descr = _('Uninstalled. Restart to stop.')
            elif name in app.started_plugins:
                descr = _('Running.')
            else:
                descr = _('Installed. Restart Ped to run.')
            lst.append((u'%s %s' % (manifest['name'], manifest['version']), descr))
        if [name for path, name, manifest in plugins if path]:
            self.menu = self.menu_plugins
            self.popup_menu = self.popup_menu_plugins
        else:
            self.menu = self.menu_empty
            self.popup_menu = self.popup_menu_empty
        if not lst:
            lst.append((_('(no plugins)'), u''))
        self.body.set_list(lst, 0)
        self.plugins = plugins

    def select_click(self):
        item = self.popup_menu.popup()
        if item:
            item.target()

    def help_click(self):
        try:
            path, name, manifest = self.plugins[self.body.current()]
            if not path:
                raise IndexError
        except IndexError:
            ui.note(_('Not available'))
            return
        bwin = ui.screen.open_blank_window(_('Please wait...'))
        path = os.path.join(path, 'help')
        helpfile = os.path.join(path, app.language.encode('utf8'))
        if not os.path.exists(helpfile):
            helpfile = os.path.join(path, 'English')
        try:
            win = HelpWindow(path=helpfile,
                title=_('Help for %s') % manifest['name'],
                head=(u'%s\n' + _('Version: %s') + u'\n\n') % (manifest['name'], manifest['version']))
            win.open()
        except IOError:
            ui.note(_('Cannot load help file'))
            bwin.close()

    def install_click(self):
        win = ui.FileBrowserWindow(title=_('Install plugin'),
            filter_ext=('.zip',))
        path = win.modal(self)
        if path:
            self.install(path)

    def uninstall_click(self):
        try:
            path = self.plugins[self.body.current()][0]
            if not path:
                raise IndexError
        except IndexError:
            ui.note(_('Not available'))
        else:
            self.uninstall(path)

    def install(self, filename):
        import zipfile
        # plugin must be a zip file
        if not zipfile.is_zipfile(filename):
            ui.note(_('Not a plugin file'), 'error')
            return
        z = zipfile.ZipFile(filename)
        lst = [x.lower() for x in z.namelist()]
        # plugin must contain the manifest and default python files
        if 'manifest.txt' not in lst or ('__init__.py' not in lst and '__init__.pyc' not in lst):
            ui.note(_('Not a plugin file'), 'error')
            return
        # parse manifest and check mandatory fields
        dct = dict([(x.lower(), x) for x in z.namelist()])
        manifest = Manifest()
        manifest.parse(z.read(dct['manifest.txt']))
        for field in ('package', 'name', 'version', 'ped-version-min', 'ped-version-max'):
            if field not in manifest:
                ui.note(_('%s field missing from manifest') % field.capitalize())
                return
        if not ui.query(_('Install\n%s %s?') % (manifest['name'], manifest['version']), 'query'):
            return
        pedver = __version__.split()[0]
        if pedver < manifest['ped-version-min']:
            ui.note(_('Requires Ped in at least version %s. Your is %s.') % (manifest['ped-version-min'], pedver), 'error')
            return
        if pedver > manifest['ped-version-max']:
            if not ui.query(_('Supports Ped up to version %s. Your is %s. Continue?') % (manifest['ped-version-max'], pedver), 'query'):
                return
            # increase version range to stop Ped from complaining upon startup
            manifest['ped-version-max'] = pedver
        path = os.path.join(self.plugins_path, manifest['package'])
        if os.path.exists(path):
            try:
                old_manifest = Manifest(os.path.join(path, 'manifest.txt'))
            except IOError:
                pass
            else:
                if not ui.query(_('Replace version %s with %s?') % (old_manifest['version'], manifest['version']), 'query'):
                    return
            self.uninstall(path, quiet=True)
        # create plugin directory
        os.mkdir(path)
        # extract plugin files to plugin directory
        def ensurepath(fullpath):
            path, name = os.path.split(fullpath)
            if path and not os.path.isdir(path):
                ensurepath(path)
            if name and not os.path.isdir(fullpath):
                os.mkdir(fullpath)
        for f in z.infolist():
            p = os.path.join(path, f.filename.replace('/', '\\'))
            pathpart, name = os.path.split(p)
            ensurepath(pathpart)
            if name:
                fh = file(p, 'wb')
                fh.write(z.read(f.filename))
                fh.close()
        z.close()
        # manifest could be changed so save it
        manifest.save(os.path.join(path, 'manifest.txt'))
        self.update()
        ui.note(_('%s %s installed') % (manifest['name'], manifest['version']), 'conf')
        ui.note(_('Restart Ped for the changes to take effect'))

    def uninstall(self, path, quiet=False):
        if not quiet:
            try:
                manifest = Manifest(os.path.join(path, 'manifest.txt'))
            except IOError:
                manifest = None
            else:
                if not ui.query(_('Uninstall\n%s %s?') % (manifest['name'],
                        manifest['version']), 'query'):
                    return
        def deldir(path):
            for name in os.listdir(path):
                filename = os.path.join(path, name)
                if os.path.isdir(filename):
                    deldir(filename)
                else:
                    try:
                        os.remove(filename)
                    except OSError:
                        pass
            try:
                os.rmdir(path)
            except OSError:
                pass
        deldir(path)
        self.update()
        if not quiet:
            if manifest is not None:
                ui.note(_('%s %s uninstalled') % (manifest['name'], manifest['version']), 'conf')
            ui.note(_('Restart Ped for the changes to take effect'))


class Application(object):
    def __init__(self):
        # path to application data
        self.path = os.path.split(sys.argv[0])[0]

        # setup i18n
        path = os.path.join(self.path, 'lang\\ped')
        try:
            alllanguages = [x.decode('utf8') for x in os.listdir(path)]
        except OSError:
            alllanguages = []
        alllanguages.append(u'English')
        alllanguages.sort(lambda a, b: -(a.lower() < b.lower()))
        # we have to load the language setting first; the real settings object
        # we will use in the app will be created later
        settings = ui.SettingsGroups(filename=os.path.join(self.path, 'settings.bin'))
        settings.append('main', ui.SettingsGroup())
        settings.main.append('language', ui.ChoiceSetting('Language', u'English', alllanguages))
        settings.try_to_load()
        self.language = settings.main.language.encode('utf8')
        if self.language != 'English':
            # load the ped language file
            translator.try_to_load(os.path.join(path, self.language))
            # load the ui language file
            path = os.path.join(self.path, 'lang\\ui')
            ui.translator.try_to_load(os.path.join(path, self.language))

        # setup settings
        allfonts = ui.available_text_fonts()
        if u'LatinBold12' in allfonts:
            defaultfont = u'LatinBold12'
        else:
            defaultfont = allfonts[0]
        allcolors = ((_('Black'), 0x000000), (_('Red'), 0x990000), (_('Green'), 0x008800), (_('Blue'), 0x000099), (_('Purple'), 0x990099))
        settings = ui.SettingsGroups(filename=os.path.join(self.path, 'settings.bin'), title=_('Settings'))
        settings.append('main', ui.SettingsGroup(_('Main'), _('Global settings')))
        settings.append('text', ui.SettingsGroup(_('Text'), _('Text windows settings')))
        settings.append('file', ui.SettingsGroup(_('File'), _('File windows settings')))
        settings.append('python', ui.SettingsGroup(_('Python'), _('Python settings')))
        settings.append('plugins', ui.SettingsGroup(_('Plugins'), _('Plugins settings')))
        settings.main.append('language', ui.ChoiceSetting(_('Language'), u'English', alllanguages))
        settings.main.append('shortcuts', ShortcutsGroupSetting(_('Global shortcuts'), RootWindow))
        settings.text.append('fontname', ui.ChoiceSetting(_('Font name'), defaultfont, allfonts))
        settings.text.append('fontsize', ui.IntegerSetting(_('Font size'), 12))
        settings.text.append('fontantialias', ui.BoolSetting(_('Font anti-aliasing'), True))
        settings.text.append('fontbold', ui.BoolSetting(_('Font bold'), False))
        settings.text.append('fontcolor', ui.ChoiceValueSetting(_('Font color'), 0x000099, allcolors))
        settings.text.append('pagesizefull', ui.IntegerSetting(_('Page size (full screen)'), 11, vmin=1, vmax=64))
        settings.text.append('pagesizeport', ui.IntegerSetting(_('Page size (portrait)'), 8, vmin=1, vmax=64))
        settings.text.append('pagesizeland', ui.IntegerSetting(_('Page size (landscape)'), 9, vmin=1, vmax=64))
        settings.text.append('shortcuts', ShortcutsGroupSetting(_('Text shortcuts'), TextWindow, True))
        settings.file.append('encoding', ui.ChoiceSetting(_('Default encoding'), 'utf-8', ('ascii', 'latin-1', 'utf-8', 'utf-16')))
        settings.file.append('autosave', ui.ChoiceValueSetting(_('Autosave'), 0, ((_('Off'), 0), (_('%d sec') % 30, 30), (_('%d min') % 1, 60), (_('%d min') % 2, 120), (_('%d min') % 5, 300), (_('%d min') % 10, 600))))
        settings.file.append('shortcuts', ShortcutsGroupSetting(_('Text file shortcuts'), TextFileWindow, True))
        settings.python.append('askforargs', ui.BoolSetting(_('Ask for arguments'), False))
        settings.python.append('shellfontcolor', ui.ChoiceValueSetting(_('Shell font color'), 0x008800, allcolors))
        settings.python.append('indentsize', ui.IntegerSetting(_('Indentation size'), 4, vmin=1, vmax=8))
        settings.python.append('fileshortcuts', ShortcutsGroupSetting(_('Python file shortcuts'), PythonFileWindow, True))
        settings.python.append('shellshortcuts', ShortcutsGroupSetting(_('Python shell shortcuts'), PythonShellWindow, True))
        self.settings = settings

        # setup file browser
        if e32.s60_version_info >= (3, 0):
            # platsec
            path = os.path.join(os.path.splitdrive(self.path)[0], '\\resource\\apps\\ped_file_browser_icons.mif')
        elif e32.s60_version_info >= (2, 8):
            # MIF files supported
            path = os.path.join(self.path, 'ped_file_browser_icons.mif')
        else:
            path = None
        if path is None or not os.path.exists(path):
            path = os.path.join(self.path, 'ped_file_browser_icons.mbm')
        ui.FileBrowserWindow.icons_path = path
        ui.FileBrowserWindow.settings_path = os.path.join(self.path, 'ped_file_browser_settings.bin')
        ui.FileBrowserWindow.add_link('C:\\Python')
        ui.FileBrowserWindow.add_link('E:\\Python')
        if e32.s60_version_info < (3, 0):
            if not ui.FileBrowserWindow.add_link('E:\\System\\Apps\\Python', _('Python Shell')):
                ui.FileBrowserWindow.add_link('C:\\System\\Apps\\Python', _('Python Shell'))

        # setup session
        TextFileWindow.session.set_filename(os.path.join(self.path, 'session.bin'))

        # properties initialization
        self.browser_win = self.help_win = self.plugins_win = None
        self.unnamed_count = 1
        self.started_plugins = {}
        
        # override __import__ to save and reload currently edited modules
        def ped_import(name, globals=None, locals=None, fromlist=None):
            # call original __import__
            mod = py_import(name, globals, locals, fromlist)
            try:
                path = mod.__file__.lower()
            except AttributeError:
                pass
            else:
                for win in ui.screen.find_windows(PythonFileWindow):
                    try:
                        if win.path.lower() == path:
                            win.save()
                            return reload(mod)
                    except AttributeError:
                        pass
            return mod
        import __builtin__
        global py_import
        py_import = __builtin__.__import__
        __builtin__.__import__ = ped_import
        
        # store the session on exit
        sys.exitfunc = TextFileWindow.store_session

    def start(self):
        # setup paths
        for path in ('C:\\Python\\lib', 'E:\\Python\\lib'):
            if os.path.exists(path):
                sys.path.append(path)

        # load and apply settings
        self.settings.try_to_load()
        self.apply_settings()

        # open root ui window (desktop)
        rootwin = RootWindow()
        rootwin.open()

        # setup main menu
        file_menu = ui.Menu(_('File'))
        file_menu.append(ui.MenuItem(_('New'), target=self.new_click))
        file_menu.append(ui.MenuItem(_('Open...'), target=self.open_click))
        main_menu = ui.Menu(_('Options'))
        main_menu.append(ui.MenuItem(_('File'), submenu=file_menu))
        main_menu.append(ui.MenuItem(_('Windows'), submenu=ui.Menu(), hidden=True))
        main_menu.append(ui.MenuItem(_('Python Shell'), target=StdIOWrapper.shell))
        main_menu.append(ui.MenuItem(_('Run Script...'), target=self.runscript_click))
        tools_menu = ui.Menu(_('Tools'))
        tools_menu.append(ui.MenuItem(_('Settings'), target=self.settings_click))
        tools_menu.append(ui.MenuItem(_('Plugins'), target=self.plugins_click))
        tools_menu.append(ui.MenuItem(_('Help'), target=self.help_click))
        tools_menu.append(ui.MenuItem(_('Orientation'), target=self.orientation_click))
        main_menu.append(ui.MenuItem(_('Tools'), submenu=tools_menu))
        main_menu.append(ui.MenuItem(_('Exit'), target=self.exit_click))
        rootwin.menu = main_menu

        # start plugins
        ui.schedule(self.start_plugins)

        # restore session
        self.restore_session()
        
        # the ui is set up now so we can simply leave and the launchpad will keep us
        # running until appuifw.app.set_exit() is called (see: RootWindow.close)

    def restore_session(self):
        TextFileWindow.session.try_to_load()
        windows = TextFileWindow.session.windows
        if windows and ui.query(_('Restore previous session?'), 'query'):
            for path, text, encoding, pos in windows:
                if text is None:
                    win = self.load_file(path)
                    if win:
                        win.body.set_pos(pos)
                else:
                    ext = os.path.splitext(path)[1].lower()
                    try:
                        klass = file_windows_types[[x.type_ext.lower() for x in file_windows_types].index(ext)]
                    except ValueError:
                        klass = TextFileWindow
                    ui.screen.open_blank_window(_('Please wait...'))
                    win = klass(title=os.path.split(path)[1].decode('utf8'))
                    if win:
                        win.body.set(text)
                        win.body.set_pos(pos)
                        win.encoding = encoding
                        if os.path.split(path)[0]:
                            win.path = path
                            win.fixed_encoding = True
                        else:
                            win.fixed_encoding = False
                        win.open()
        del windows[:]
        try:
            TextFileWindow.session.save()
        except IOError:
            ui.note(_('Cannot update session file'), 'error')

    def start_plugins(self):
        plugins_path = os.path.join(self.path, 'plugins')
        self.started_plugins = {}
        allkeys = self.settings.allkeys()

        for name in os.listdir(plugins_path):
            path = os.path.join(plugins_path, name)
            if not os.path.isdir(path):
                continue

            # load manifest
            try:
                manifest = Manifest(os.path.join(path, 'manifest.txt'))

                # check version
                pedver = __version__.split()[0]
                if manifest['ped-version-min'] > pedver:
                    ui.note(_('%s plugin requires Ped in at least version %s. Your is %s. Skipping.') % \
                        (manifest['name'], manifest['ped-version-min'], pedver), 'error')
                    continue
                if pedver > manifest['ped-version-max']:
                    if not ui.query(_('%s plugin supports Ped up to version %s. Your is %s. Run?') % \
                            (manifest['name'], manifest['ped-version-max'], pedver), 'query'):
                        continue
                    manifest['ped-version-max'] = pedver
                    manifest.save(os.path.join(path, 'manifest.txt'))

                __import__('plugins.%s' % name)
                self.started_plugins[name] = (manifest['name'], manifest['version'])

            except:
                from traceback import print_exc
                print_exc()
                ui.note(_('Starting "%s" plugin failed, skipping') % name.decode('utf8'), 'error')

        if self.settings.allkeys() != allkeys:
            # plugins have added/removed the settings;
            # reload so the new settings are loaded too
            self.settings.try_to_load()
            self.apply_settings()

        ui.screen.redraw()
    
    def exit_click(self):
        if ui.screen.find_windows(TextFileWindow):
            menu = ui.Menu(_('Exit'))
            menu.append(ui.MenuItem(_('Close all files'), store=False))
            menu.append(ui.MenuItem(_('Store the session'), store=True))
            item = menu.popup()
            if item is None:
                return
            if item.store:
                TextFileWindow.store_session()
                ui.screen.rootwin.shutdown()
                return
        ui.screen.rootwin.close()
        
    def new_file(self, klass):
        title = 'Unnamed%d%s' % (self.unnamed_count, klass.type_ext)
        self.unnamed_count += 1
        win = klass(title=title)
        win.open()
        return win

    def settings_click(self):
        if self.settings.edit():
            self.settings.save()
            self.apply_settings()

    def apply_settings(self):
        GlobalWindowModifier.update_settings()
        if self.language != self.settings.main.language.encode('utf8'):
            ui.note(_('Restart Ped for the changes to take effect'))

    def plugins_click(self):
        if self.plugins_win and self.plugins_win.is_opened():
            self.plugins_win.focus = True
            return
        self.plugins_win = PluginsWindow()
        self.plugins_win.open()

    def help_click(self):
        if self.help_win and self.help_win.is_opened():
            self.help_win.focus = True
            return
        bwin = ui.screen.open_blank_window(_('Please wait...'))
        path = os.path.join(self.path, 'lang\\help')
        helpfile = os.path.join(path, self.language)
        if not os.path.exists(helpfile):
            helpfile = os.path.join(path, 'English')
        try:
            self.help_win = HelpWindow(path=helpfile,
                head=u'Ped - Python IDE\n'
                     u'Version: %s\n'
                     u'\n'
                     u'Copyright \u00a9 2007-2008\nArkadiusz Wahlig\n'
                     u'<arkadiusz.wahlig@gmail.com>\n'
                     u'\n' % __version__)
            self.help_win.open()
        except IOError:
            ui.note(_('Cannot load help file'), 'error')
            bwin.close()

    def new_click(self):
        menu = ui.Menu(_('New'))
        for klass in file_windows_types:
            menu.append(ui.MenuItem(klass.type_name, klass=klass))
        item = menu.popup()
        if item:
            self.new_file(item.klass)

    def open_click(self):
        if self.browser_win:
            ui.note(_('File browser already in use'), 'error')
            return
        self.browser_win = ui.FileBrowserWindow(title=_('Open file'))
        path = self.browser_win.modal()
        self.browser_win = None
        if not path:
            return
        self.load_file(path)

    def load_file(self, path):
        # check if this file isn't already opened
        for win in ui.screen.find_windows(TextFileWindow):
            if win.path == path:
                win.focus = True
                return
        ext = os.path.splitext(path)[1].lower()
        try:
            klass = file_windows_types[[x.type_ext.lower() for x in file_windows_types].index(ext)]
        except ValueError:
            klass = TextFileWindow
        wwin = ui.screen.open_blank_window(_('Please wait...'))
        try:
            win = klass(path=path)
            win.open()
        except IOError:
            win = None
            ui.note(_('Cannot load %s file') % os.path.split(path)[1], 'error')
            wwin.close()
        return win

    def runscript_click(self):
        if self.browser_win:
            ui.note(_('File browser already in use'), 'error')
            return
        self.browser_win = ui.FileBrowserWindow(title=_('Run script'))
        path = self.browser_win.modal()
        self.browser_win = None
        if not path:
            return
        if self.settings.python.askforargs:
            menu = ui.Menu(_('Arguments'))
            menu.append(ui.MenuItem(_('Edit...')))
            menu.append(ui.MenuItem(_('No arguments')))
            item = menu.popup()
            if not item:
                return
            if item.name == 'edit':
                args = ui.query(_('Arguments:'), 'text')
                if not args:
                    return
            else:
                args = u''
            args = quote_split(args.encode('utf8'))
        else:
            args = []
        shell = StdIOWrapper.shell()
        if shell.is_busy():
            return
        shell.restart()
        shell.enable_prompt(False)
        shell.lock(True)
        from linecache import checkcache
        checkcache()
        TextFileWindow.store_session()
        # list() will make copies so we will be able to restore these later
        mysys = list(sys.argv), list(sys.path)
        sys.path.insert(0, os.path.split(path)[0])
        sys.argv = [path] + args
        modules = sys.modules.keys()
        try:
            execfile(path, shell.py_namespace)
        finally:
            for m in sys.modules.keys():
                if m not in modules:
                    del sys.modules[m]
            sys.argv, sys.path = mysys
            TextFileWindow.clear_session()
            shell = StdIOWrapper.shell()
            shell.lock(False)
            ui.screen.redraw()
            shell.enable_prompt(True)

    def orientation_click(self):
        win = ui.screen.focused_window()
        ori = newori = win.orientation
        if ori == ui.oriAutomatic:
            w, h = ui.layout(ui.EApplicationWindow)[0]
            if w > h:
                newori = ui.oriPortrait
            else:
                newori = ui.oriLandscape
        else:
            win.orientation = ui.oriAutomatic
            w, h = ui.layout(ui.EApplicationWindow)[0]
            if ori == ui.oriPortrait:
                if w > h:
                    newori = ui.oriAutomatic
                else:
                    newori = ui.oriLandscape
            elif ori == ui.oriLandscape:
                if w > h:
                    newori = ui.oriPortrait
                else:
                    newori = ui.oriAutomatic
        if newori != ori:
            for win in ui.screen.find_windows():
                win.orientation = newori
                if isinstance(win, TextWindow):
                    win.reset_caret()


class ShortcutsGroupSetting(ui.GroupSetting):
    def __init__(self, title, klass, none_item=False):
        ui.GroupSetting.__init__(self, title)
        self.klass = klass
        self.none_item = none_item
        self.info = _('(more options)')

    def set(self, value):
        self.value.clear()
        for key, val in value.items():
            setting = self.get_new(_('Green-%s') % chr(key), val)
            if setting is not None:
                self.value.append(key, setting)
        self.value.sort()
        self.original = self.value.items()

    def to_item(self, setting):
        if setting is not None:
            return (unicode(setting.title), unicode(setting))
        else:
            return (_('(no shortcuts)'), u'')

    def get_new(self, title, value=''):
        choices = [(item.title, name) for name, item in \
            self.klass.get_shortcuts_items()]
        if self.none_item:
            choices.insert(0, (_('(none)'), ''))
        return ui.ChoiceValueSetting(title, value, choices)

    def get_new_name(self):
        menu = ui.Menu(_('Choose key'))
        for ckey in '1234567890*#':
            menu.append(ui.MenuItem(ckey))
        menu.append(ui.MenuItem(_('Custom...')))
        item = menu.popup()
        if item is not None:
            if item is menu[-1]:
                ckey = u''
                while True:
                    ckey = ui.query(_('Choose key'), 'text', ckey)
                    if ckey is None:
                        return
                    if len(ckey) == 1:
                        break
                    ui.note(_('Enter one key only'))
            else:
                ckey = item.title
            ckey = ckey[0].lower()
            key = ord(ckey)
            return key, _('Green-%s') % ckey

    def __str__(self):
        return str(self.info)
        
    def __unicode__(self):
        return unicode(self.info)

class Manifest(object):
    def __init__(self, filename=None):
        self.fields = {}
        if filename is not None:
            self.load(filename)
        
    def parse(self, data):
        lines = data.decode('utf8').splitlines()
        lines.reverse()
        self.fields = {}
        while True:
            try:
                ln = lines.pop()
            except IndexError:
                break
            try:
                p = ln.index(u':')
            except ValueError:
                raise ValueError('mangled manifest file')
            name = ln[:p].strip().title()
            value = []
            vln = ln[p+1:].strip()
            while vln.endswith(u'\\'):
                value.append(vln[:-1].strip())
                try:
                    ln = lines.pop()
                except IndexError:
                    break
                vln = ln.strip()
            else:
                value.append(vln)
            if name in self.fields:
                raise ValueError('manifest field defined twice')
            self.fields[name] = u'\r\n'.join(value)
    
    def dump(self):
        lines = []
        for name, value in self.fields.items():
            lines.append(u'%s: %s\r\n' % (name.title(), '\\\r\n'.join(value.split('\n'))))
        return (''.join(lines)).encode('utf8')

    def load(self, filename):
        f = open(filename, 'r')
        try:
            self.parse(f.read())
        finally:
            f.close()

    def save(self, filename):
        f = open(filename, 'w')
        try:
            f.write(self.dump())
        finally:
            f.close()

    def get(self, name, default=None):
        return self.fields.get(name.title(), default)

    def keys(self):
        return self.fields.keys()
        
    def items(self):
        return self.fields.items()
        
    def values(self):
        return self.fields.values()

    def clear(self):
        self.fields = {}

    def __getitem__(self, name):
        return self.fields[name.title()]
    
    def __setitem__(self, name, value):
        self.fields[name.title()] = value

    def __delitem__(self, name):
        del self.fields[name.title()]
        
    def __len__(self):
        return len(self.fields)
        
    def __contains__(self, name):
        return name.title() in self.fields


def quote_split(s):
    # like s.split() but sentences in quotes
    # are treated as one word
    s += ' '
    ret = []
    for x in s.split('"'):
        if x:
            i = s.index(x)
        else:
            i = s.index('""') + 1
        try:
            if s[i-1] == '"' and s[i+len(x)] == '"':
                ret.append(x)
                s = s[:i-1] + s[i+len(x)+1:]
                continue
        except IndexError:
            pass
        ret += x.split()
        s = s[:i] + s[i+len(x):]
    return ret


def repattr(obj, name, value):
    '''Sets an attribute of a class/object. Returns the old value.
    '''
    old = getattr(obj, name)
    setattr(obj, name, value)
    return old
    
    
def get_plugin_translator(plugin_path):
    '''Returns a new ui.Translator object for plugin specified
    by plugin_path argument. If a file is passed, the last
    path component is removed.
    '''
    if os.path.isfile(plugin_path):
        plugin_path = os.path.split(plugin_path)[0]
    path = os.path.join(plugin_path, 'lang\\' + app.language)
    trans = ui.Translator()
    translator.try_to_load(path)
    return trans


# i18n object
translator = _ = ui.Translator()


# Supported file types
file_windows_types = [TextFileWindow, PythonFileWindow]


# Application object
app = Application()
